package com.faya.green.customize;

import com.faya.green.abnormal.ServiceException;
import com.faya.green.model.Role;
import com.faya.green.model.User;
import com.faya.green.model.web.UserRoleAuthor;
import com.faya.green.service.AuthorityService;
import com.faya.green.service.RoleService;
import com.faya.green.service.SysLogService;
import com.faya.green.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 用户 ：LX
 * 创建时间： 2018/7/10. 17:14
 * 地点：广州
 * 目的: 自定义的realm ，相当于shiro的dao层，去查询获取 账号密码校验；权限等 信息
 *
 * 在认证、授权内部实现机制中都有提到，最终处理都将交给Real进行处理。因为在Shiro中，最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下，在Realm中会直接从我们的数
 * 据源中获取Shiro需要的验证信息。可以说，Realm是专用于安全框架的DAO.
 *
 * Shiro的认证过程最终会交由Realm执行，这时会调用Realm的getAuthenticationInfo(token)方法。 该方法主要执行以下操作:
     * 1、检查提交的进行认证的令牌信息
     * 2、根据令牌信息从数据源(通常为数据库)中获取用户信息
     * 3、对用户信息进行匹配验证。
     * 4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
     * 5、验证失败则抛出AuthenticationException异常信息。
 *
 * 结果：
 */
public class MyRealm extends AuthorizingRealm {

    private static Logger log = LoggerFactory.getLogger(MyRealm.class);

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private AuthorityService authorityService;



    /**
     * 授权
     *  决定那些角色可以使用那些资源
     * 会进入授权方法一共有三种情况：
     *      1 自己去调用这个是否有什么角色或者是否有什么权限的时候；
     *      2 @RequiresRoles("admin") ：在方法上加注解的时候；
     *      3 [@shiro.hasPermission name = "admin"][/@shiro.hasPermission]：在页面上加shiro标签的时候，即进这个页面的时候扫描到有这个标签的时候
     *
     *   数据库查询到的数据是 通配符形式的，如 user:create 这种，角色是单纯的字符串
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User)SecurityUtils.getSubject().getPrincipal();
        log.debug(user.getId() + ":执行授权操作中........");
        //简单的授权信息
        SimpleAuthorizationInfo simpleAuthorizationInfo =  new SimpleAuthorizationInfo();

        //角色
        Set<String> rolesName = new HashSet<String>();
        //数据库根据用户ID查询它拥有的角色
        List<Role> roleList = roleService.getUserByRole(user.getId());
        if (roleList != null && roleList.size() > 0){
            for (Role role : roleList){
                rolesName.add(role.getName());
            }
        }

        //权限
        Set<String> authors = new HashSet<String>();
        //数据库根据用户ID查询它拥有的权限
        List<UserRoleAuthor> userRoleAuthors = authorityService.getUserRoleAuthorByUid(user.getId());
        if (userRoleAuthors != null && userRoleAuthors.size() > 0){
            for (UserRoleAuthor userRoleAuthor : userRoleAuthors){
                authors.add(userRoleAuthor.getAboundary());
            }
        }

        //将数据放入到其中
        //放入角色名
        simpleAuthorizationInfo.setRoles(rolesName);
        //放入权限
        simpleAuthorizationInfo.setStringPermissions(authors);
        log.debug(user.getId() + ":执行授权操作结束........");

        return simpleAuthorizationInfo;
    }

    /**
     * 登录认证
     *  判断一个请求是否被允许进入系统
     *      什么时候调用：
     *           当执行 Subject currentUser = SecurityUtils.getSubject();currentUser.login(token); 调用
     * @param token 认证token
     * @return AuthenticationInfo,就是认证信息
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        //从token中获取用户名，这个token是从登录的controller中获取的
        String username = usernamePasswordToken.getUsername();
        String password = "";
        if (usernamePasswordToken.getPassword() != null){
            password = new String(usernamePasswordToken.getPassword());
        }

        User user = null;
        try {
            //登录验证
            user = userService.login(username, password);

        } catch (UnknownAccountException e){
            e.printStackTrace();
            log.debug(username + " 登录系统失败,错误信息：" + e.getMessage());
            throw new UnknownAccountException(e.getMessage());
        } catch (IncorrectCredentialsException e){
            e.printStackTrace();
            log.debug(username + " 登录系统失败,错误信息：" + e.getMessage());
            throw new IncorrectCredentialsException(e.getMessage());
        } catch (LockedAccountException e){
            e.printStackTrace();
            log.debug(username + " 登录系统失败,错误信息：" + e.getMessage());
            throw new LockedAccountException(e.getMessage());
        } catch (DisabledAccountException e){
            e.printStackTrace();
            log.debug(username + " 登录系统失败,错误信息：" + e.getMessage());
            throw new DisabledAccountException(e.getMessage());
        } catch (AuthenticationException e) {
            e.printStackTrace();
            log.debug(username + " 登录系统失败,错误信息：" + e.getMessage());
            throw new AuthenticationException(e.getMessage(), e);
        } catch (ServiceException e){
            e.printStackTrace();
            log.debug(username + " 登录系统失败,错误信息：" + e.getMessage());
            throw new ServiceException(e.getMessage());
        }

        //第一个参数user，这里传的是user对象
        //第二个参数注意这里是放用户传入的密码,真正的密码校验不是在这里，这里只是获取用户信息，按照逻辑上，这里应该放数据库中存储的加密密码
        //这里是将校验放到了用户service层了，在那里进行了校验，所以这里要将用户传入的明文密码放入。否则正常是从token中获取到加密后的密码，这里放入也是数据库存的密码，shiro会自动对比密码
        //第三个字段是realm，即当前realm的名称
        //这里插入了对象后，在freemarker中可以直接使用 <@shiro.principal property="username"/> 获取到用户名，其中property是对象的属性，如果直接放的字符串，则不需要指定property，直接<@shiro. principal/>即可
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }

    /**
     * 当用户权限修改后，用户再次登陆shiro会自动调用realm从数据库获取权限数据，如果在修改权限后想立即清除缓存则可以调用realm的clearCache方法清除缓存
     *
     * 调用方式：
         *  RealmSecurityManager rsm = (RealmSecurityManager)SecurityUtils.getSecurityManager();
         *  AuthRealm authRealm = (AuthRealm)rsm.getRealms().iterator().next();
         *  authRealm.clearCached();

     */
    public void clearCached() {
        log.debug("执行清空权限缓存");
        PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }
}
